/**
 * Hvm 双向绑定模板引擎 v1.0
 * @description 可以直接在DOM中添加数据，类似VUE，与VUE原理相同
 * @support IE9，Chrome，FireFox，safari
 * @author HuiSir宫晓辉(原理部分参考[canfoo](http://www.cnblogs.com/canfoo/p/6891868.html))
 * @added 与canfoo所写的self-vue版不同的是：
 * 		1.整体修改，重写编译器，通篇注释并优化代码
 * 		2.加入其他表单类型的双向绑定，如select、radio等
 * 		3.model指令支持多级json数据，如h-model="a.b.c"
 * 		4.{{}}指令支持表达式如:{{name}},{{name.a}},{{arr.length}},{{a.arr.length}},{{a+b-c/d*e}},{{a?b:c}}
 * 		5.属性值支持{{}}如：data-uid="{{userid}}"
 * 		6.使同一节点允许{{}}外有其他字符串如：<p>{{name}}:19岁</p>
 * 		7.指令：条件渲染指令h-if、显隐指令h-show、事件指令@、列表渲染语句h-for(h-for指令有待优化)
 * @myBlog http://www.zuifengyun.com
 * @example 注意：在同一个节点里，'{{}}'不能使用两次如：<p>{{a}}-{{b}}</p>是不允许的，可以写为<p>{{a+'-'+b}}</p>
 * @example 注意：h-for指令所在节点内部或其子节点不允许出现所循环数组以外的数据。也就是只能出现所循环的数组的item或数组相关属性
 * @example 注意：h-for指令规范：h-for="item in arr"(注意三个单词中间空格只有一个，否则会出错)；h-for="(item,index) in arr"注意括号内用逗号分隔，不能有多余空格
 * @example if条件指令插入节点使用的insertBefore方法，节点渲染使用的是fragment，可能性能有所欠缺。不是虚拟DOM，这是与jsx或VUE,angular区别
 * @example 在输入框中可以使用h-model属性绑定数据，实现输入框的反向绑定(支持文本输入框、select单选框、radio等，不支持select多选和checkbox)
 * @example 可在元素中使用@click属性绑定事件，实现事件的绑定（由于是动态更新DOM，所以不方便直接使用类似与“元素.onclick=function(){}”方式监听事件）
 * @example 所有使用@指令绑定的事件都写到vm的methods对象内
 * @createTime 20190315
 * @update 20190329
 */

//273行  h-for指令未实现


/* 原理 */
/* Compile(编译器)--[更新]-->View(视图层) */
/* Observer(劫持并监听所有属性)--[通知变化]-->Dep(订阅者容器)--[通知变化]-->Watcher(订阅者/观察者)--[回调]-->Compile(解析器)--[更新]-->View(视图层) */
/* Compile(编译器)--[绑定更新函数]-->Watcher(订阅者)--[添加订阅者]-->Dep(订阅者容器)--[通知变化]-->Watcher(订阅者)--[回调]-->Compile(解析器)--[更新]-->View(视图层) */

/**
 * 引擎对象(类似于VUE对象)
 * 命名中的H是我英文名HuiSir的首字母H
 */
var Hvm = function(options) {
	//判断data存不存在
	if (!options.data || typeof options.data !== 'object') {	
		throw new Error('data对象不存在或格式不对！')
	    return
	}
	if(!options.el || typeof options.el !== 'string' || (options.el.substring(0,1)!='#' && options.el.substring(0,1)!='.')){
		throw new Error('模板引擎需要绑定的元素el不存在，为空或不是以#或.开头！')
		return
	}
	var _this = this
	this.data = options.data	//数据
	this.methods = options.methods?options.methods:{}	//方法
	Object.keys(this.data).forEach(function(key) {
        _this.proxyKeys(key)	//属性代理，可以使用this.a访问data对象中的a字段
    })
	new Observer(this.data)	//监听数据对象
	if(options.beforeOnload && typeof options.beforeOnload == 'function'){
		options.beforeOnload.call(this)	//执行DOM解析渲染前钩子(call方法立即执行)
	}
	new Compile(options.el, this)	//解析渲染
	if(options.onload && typeof options.onload == 'function'){
		options.onload.call(this)	//执行加载完成钩子
	}
}

Hvm.prototype = {
	proxyKeys: function (key) {	//属性代理，可以使用this.a访问data对象中的a字段
        var _this = this
        Object.defineProperty(this, key, {	//使用this对象代理this.data的属性（defineProperty-定义属性）
            enumerable: false,	//不可枚举，也就是在vm对象中实际不存在，不能用for...in或Object.keys方法取出
            configurable: true,	//允许读写
            get: function getter () {	//访问
                return _this.data[key]
            },
            set: function setter (newVal) {	//修改
                _this.data[key] = newVal
            }
        })
    }
}

/**
 * 监听器Observer
 * 用来劫持并监听所有属性，如果有变动的，就通知订阅者
 */
var Observer = function (data) {
    this.data = data
    this.start(data)	//开始数据劫持
}

Observer.prototype = {
	start:function(data) {	//开始：将数据遍历，循环调用监听方法
		var _this = this
		Object.keys(data).forEach(function(key) {	//遍历key数组
			_this.monitor(data,key,data[key])	//循环调用监听器以便监听所有key
		})
	},
	
	monitor:function(data, key, val) {	//监听：这里用到Object.defineProperty()方法进行监控数据的读写
		var dep = new Dep() //创建新的订阅者容器(每个不同的key都有多个订阅者，一个容器里是同一个key的订阅者)
		var childObj
		if (val && typeof val == 'object' && Object.keys(val).length>0) {	//过滤器，若存在子级对象，则循环监听器
			childObj = new Observer(val)
		}
		Object.defineProperty(data, key, {	//监控器
            enumerable: true,	//允许对象枚举，可用for...in或Object.keys方法取出
            configurable: true,	//允许对象读写
            get: function getter () {	//对象读取回调
                if (Dep.cache) {	//判断容器缓存中是否有新订阅者
                    dep.add(Dep.cache)	//添加此订阅者(以便新的订阅者可以读到数据)
                }
                return val
            },
            set: function setter (newVal) {	//对象修改回调(返回的参数为新的值)
                if (newVal === val) {	//如果修改后的值和原值相同，则不操作
                    return
                }
                val = newVal	//替换旧值
                dep.notify()	//遍历所有订阅者并通知其需要更改了
            }
        })
	}
}

/**
 * 订阅者容器(数组)Dep
 * 用来劫持并监听所有属性，如果有变动的，就通知订阅者
 */
var Dep = function() {
	this.watchers = []
}

//新订阅者缓存器,不放到Dep方法中是为避免创建Dep对象时缓存器被清空
//缓存器的作用是告诉监听器在读数据时多会需要新增订阅者
Dep.cache = null	

Dep.prototype = {
    add: function(child) {	//新增订阅者
        this.watchers.push(child)
    },
    notify: function() {	//通知每一个订阅者(notify-通知)
        this.watchers.forEach(function(child) {	//遍历某容器所有订阅者
            child.update()	//更新每一个订阅者的数据
        })
    }
}

/**
 * 订阅者(观察者)Watcher
 * 可以收到属性的变化通知并执行相应的函数，从而更新视图。
 * 相当于在视图中多次读取了同一个数据，每一次读取都是一个订阅者。
 * 如<p>{{name}}{{name}}</p>中就有两个订阅者。
 * @param vm [Object] 虚拟机模板引擎对象Hvm(携带数据，类似于VUE本身对象)
 * @param keyName [String] 每一个DOM片段携带的数据的key键名，在解析器中使用正则的exec方法获取
 * @param callback [Function] 回调函数，返回解析器
 */
var Watcher = function(vm,keyName,callback) {
	this.callback = callback
    this.vm = vm
    this.keyName = keyName
    this.value = this.get()  //将自己添加到订阅器，返回自己
}

Watcher.prototype = {
	update:function() {	//回调方式通知编译器更新某个订阅者的视图
		var __value
		with(this.vm.data){
			try{
				__value = eval(this.keyName)	//新值(this.keyName有可能包含多层如a.b.c所以这里用eval方法)
			}catch(err){
				__value = ''
			}
		}
        var oldVal = this.value		//旧值
        if (__value !== oldVal) {
            this.value = __value	//旧值刷新
            this.callback.call(this.vm, __value, oldVal)	//回调
        }
	},
	get:function() { //添加到订阅器(只要获取属性值就可以触发监听器get方法，然后添加到订阅器)
		Dep.cache = this  // 缓存自己
		var __value
		with(this.vm.data){
			try{
				__value = eval(this.keyName)	//获取data中的属性值,触发监听器里的get函数
			}catch(err){
				__value = ''
			}
		}
        Dep.cache = null  // 释放缓存器
        return __value
	}
}

/**
 * 解析器Compile(Compile-编译)
 * 可以扫描和解析每个节点的相关指令，并根据初始化模板数据以及初始化相应的订阅器。
 * @param el [String] 绑定的DOM节点，Hvm容器节点
 * @param vm [Object] Hvm引擎对象
 */
var Compile = function(el, vm) {
    this.vm = vm
    this.el = document.querySelector(el)	//Hvm容器节点
    this.fragment = null	//节点碎片对象
	this.ifDirCache = {}	//h-if指令所在相关节点缓存器
    this.init()		//注册(编译器入口)
}

Compile.prototype = {
    init: function () {	//入口方法
		this.fragment = this.nodeToFragment(this.el)	//节点转换为碎片对象
		this.compileElement(this.fragment)	//解析元素，读取其中的{{}}数据语法
		this.el.appendChild(this.fragment)	//初始化页面
    },
    nodeToFragment: function (el) {	//将节点转换为碎片对象
        var fragment = document.createDocumentFragment();
        var child = el.firstChild;
        while (child) {	//如果child存在，会一直循环，直到el的所有子节点都添加到fragment中
            fragment.appendChild(child)	// 将Dom元素移入fragment中
            child = el.firstChild	//appendChild()方法会移除原el中的它自己
			//所以此el.firstChild并不是原先的firstChild，而是第二个子节点
			//那么fragment中的节点是按照el原顺序排列的
        }
        return fragment
    },
    compileElement: function (el) {	//解析元素，读取其中的{{}}数据语法
        var childNodes = el.childNodes	//获取碎片中的子元素
        var self = this;
        [].slice.call(childNodes).forEach(function(node) {	//[].slice.call()用于将类数组转换为数组
            var reg = /\{\{([^\{\}]+)?\}\}/;	//正则匹配{{}}，排除子表达式还包含{}的情况
												//同一个节点范围内只允许有一个{{}}指令，所以这里不能加g标志
            var text = node.textContent;	//获取node节点字符串
			var attrNameArr = []
            if (self.isElementNode(node)) { //元素节点 
				//循环元素的属性，将其属性名取出，以便在稍后判断是否含有h-for指令
				Array.prototype.slice.call(node.attributes).forEach(function(attr){
					attrNameArr.push(attr.name)
				})
                self.compile(node,reg,attrNameArr)	//解析元素节点内容
            } else if (self.isTextNode(node) && reg.test(text)) {	//文本节点且包含{{}}指令
                self.compileText(node, reg.exec(text)[1], reg, text)
            }
			
			//判断元素节点是否含有h-for指令，如果含有，则其子节点不重新循环渲染
			if(!self.isElementNode(node) || attrNameArr.indexOf('h-for')<0){	
				//如果节点包含子节点，循环自身
				if (node.childNodes && node.childNodes.length) {	
					self.compileElement(node);
				}
			}
			
        })
    },
    compile: function(node, reg,attrNameArr) {	//解析器(核心)
        var nodeAttrs = node.attributes	//读取元素属性
        var self = this
        Array.prototype.forEach.call(nodeAttrs, function(attr) {	//类数组使用数组的forEach循环方法
            var attrName = attr?attr.name:''	//属性名
			var	attrVal = attr?attr.value:''	//属性值
			/* 解析'h-'指令 */
            if (self.isDirective(attrName)) {	//是否含有'h-'指令
                var dir = attrName.substring(2)	//截取属性指令(如'h-model'中的model)
				switch (dir){
					case 'model':
						self.compileModel(node, attrVal)	//解析model指令
						break;
					case 'if':
						//这里在h-if指令所在的节点之后创建空文本节点的作用是：防止俩个h-if指令出现在相邻位置
						//如若有一方被移除,那么它之前的节点就会无法找到兄弟节点nextSibling而不能插入
						//这样操作可使所有携带h-if指令的节点的nextSibling兄弟节点都是一个空文本节点
						node.parentNode.insertBefore(document.createTextNode(''),node.nextSibling)
						self.compileIf(node,attrVal)	//解析h-if指令
						break;
					case 'show':
						self.compileShow(node,attrVal)	//解析h-show指令
						break;
					case 'for':
						var forFrag = self.nodeToFragment(node)	//节点转换为碎片对象
						//this.compileElement(this.fragment)	//解析元素，读取其中的{{}}数据语法
						node.appendChild(forFrag)	//初始化页面
						self.compileFor(node,attrVal)	//解析h-for指令
						break;
				}
            }
			/* 解析'@'事件指令 */
			if (self.isEventDirective(attrName)) {  //事件指令
				var dir = attrName.substring(1)	//截取事件指令(如'@click'中的click)
			    self.compileEvent(node, self.vm, attrVal, dir)	//解析事件
			} 
			/* 解析属性值中的'{{}}'数据值 */
			if(attrNameArr.indexOf('h-for')<0 && reg.test(attrVal)){	//注意若为h-for指令，暂不解析其属性值
				var exp = reg.exec(attrVal)[1]
				var initText = self.compile2Text(exp)	//将{{}}内容解析为真实数据
				//替换原属性值中的内容(考虑到属性值可能为data-name="{{username}}aaa"的情况)
				var newVal = attrVal.replace(reg.exec(attrVal)[0],initText)
				node.setAttribute(attrName, newVal)
			}
        })
    },
    compileText: function(node, exp, reg, text) {	//文本解析器
        var self = this
        var initText = this.compile2Text(exp)	//将{{}}内容解析为真实数据
        this.updateText(node, initText,reg,text)	//更新数据
        new Watcher(this.vm, exp, function (value) {	//将自己添加到订阅器(创建实例对象)
            self.updateText(node, value,reg,text)	//如若修改数据，则跟新此订阅器的值
        })
    },
    compileEvent: function (node, vm, exp, dir) {	//事件解析器
        var cb = vm.methods && vm.methods[exp]	
		//这里的cb为vm.methods[exp]，&&短路，如果左侧为假，返回左侧的值，反之返回结果为&&右侧的值
        if (dir && cb) {
            node.addEventListener(dir, cb.bind(vm), false);
			//这里用bind方法原因是，vm.methods[exp]函数是通过new对象创建的出来的实例，与当前对象虽然是同一对象，但是是实例对象，原先本不存在
			//直接调用会丢掉上下文，丢失原对象，使得vm.methods[exp]这个函数指针指向的是当前作用域
        }
    },
    compileModel: function (node, exp) {	//model解析器(反向绑定)
        var self = this;
        var val = this.compile2Text(exp)
        this.modelUpdater(node, val);	//初始化当前输入框的值
        new Watcher(this.vm, exp, function (value) {	//将自己添加到订阅器(创建实例对象)
            self.modelUpdater(node, value);	//如若修改数据，则跟新此订阅器的值(此输入框的值)
        })
		//监听
        if(node.type=='select-one'){	//select单选下拉框
        	node.addEventListener('change', function(e) {
				eval('self.vm.'+exp+'=e.target.value')	//写值(exp可能是多层对象如a.b.c所以用eval方法比较保险)
        	})
        }else if(node.type=='radio'){	//radio单选
        	node.addEventListener('change', function(e) {
				eval('self.vm.'+exp+'=e.target.value')	//写值
        	})
        }else{
        	node.addEventListener('input', function(e) {
        	    var newValue = e.target.value;
        	    if (val === newValue) {	//没改
        	        return;
        	    }
				eval('self.vm.'+exp+'=newValue')	//写值
        	    val = newValue;	//新值变为旧值
        	})
        }
    },
	compileIf:function(node, exp){	//h-if指令解析器
		var self = this;
		var val = this.compile2Text(exp)
		var pName = 'pa'+ Object.keys(this.ifDirCache).length	//父节点容器唯一字段名，用于缓存到this.ifDirCache
		var nName = 'next'+ Object.keys(this.ifDirCache).length	//拼接ifDirCache的长度以便不会重复
		this.ifDirCache[nName] = node.nextSibling	//兄弟节点缓存
		if(!val){
			this.ifUpdater(node,val,pName,nName)	//如果值为false，初始化当前节点显隐情况
		}
		new Watcher(this.vm, exp, function (value) {	//将自己添加到订阅器(创建实例对象)
		    self.ifUpdater(node,value,pName,nName)		//如若修改数据，则跟新此订阅器的值
		})
	},
	compileShow:function(node, exp) {	//h-show指令解析器
		var self = this;
		var val = this.compile2Text(exp)
		this.showUpdater(node,val)	//初始化当前节点显隐情况
		new Watcher(this.vm, exp, function (value) {	//将自己添加到订阅器(创建实例对象)
		    self.showUpdater(node,value)		//如若修改数据，则跟新此订阅器的值
		})
	},
	compileFor:function(node, exp) {	//h-for指令解析器
		var forValArr = exp.split(' ') , _this = this
		var itemKey = forValArr[0].indexOf('(')<0 ? forValArr[0] : forValArr[0].split(',')[0].slice(1) , str = forValArr[0].split(',')[1]
		var indexKey = forValArr[0].indexOf('(')<0 ? null : str.slice(0,str.length-1)
		var realData = this.compile2Text(forValArr[2])
		this.forUpdater(node,forValArr[2],realData,itemKey,indexKey)	//初始化当前节点视图
		new Watcher(this.vm, forValArr[2], function (value) {	//将自己添加到订阅器(创建实例对象)
		    _this.forUpdater(node,forValArr[2],value,itemKey,indexKey)		//如若修改数据，则跟新此订阅器的值
		})
	},
	compile2Text:function(exp) {	//将'{{}}'中的内容解析为真实数据字符串，支持部分js语句，支持多层json
		var __initText	//加双下划线 防止with语句中出现同名变量
		with(this.vm){	//with方法将data作用域放置到全局作用域中，使得可以直接使用a变量访问data中的a字段值
			try{
				__initText = eval(exp)	//字符串要用eval转变为js进行运行
			}catch(err){	//避免DOM中的变量在data中未定义
				__initText = ''
			}
		}
		return __initText
	},
    isElementNode: function (node) {	//判断是否为元素节点
		//nodeType=1元素节点，2属性节点，3元素或属性中的文本内容，8代表注释
        return node.nodeType == 1;
    },
    isTextNode: function(node) {	//判断是否为文本节点
        return node.nodeType == 3;
    },
    isDirective: function(attr) {	//属性'h-'指令，实现反向绑定
        return attr.indexOf('h-') == 0;
    },
    isEventDirective: function(attr) {	//事件指令
        return attr.indexOf('@') === 0;
    },
    updateText: function (node, value,reg,text) {	//更新数据，如若获取不到则显示空字符串（视图层）
		//textContent可读可写
		node.textContent = typeof value == 'undefined' ? text.replace(reg.exec(text)[0],''):text.replace(reg.exec(text)[0],value)
    },
    modelUpdater: function(node, value, oldValue) {	//更新model数据（视图层）
        if(node.type=='radio'){
    		node.checked = node.value==value?true:false
    	}else{
    		node.value = typeof value == 'undefined' ? '' : value;
    	}
    },
	ifUpdater: function(node,val,pName,nName){	//更新h-if指令所在节点的存在情况（视图层）
		//缓存父节点容器：这里注意父节点可能是fragment对象，要转换为this.el节点，否则无法插入
		if(node.parentNode) this.ifDirCache[pName] = node.parentNode.nodeName=='#document-fragment'?this.el:node.parentNode
		if(val){
			this.ifDirCache[pName].insertBefore(node,this.ifDirCache[nName])
		}else{
			node.parentNode.removeChild(node)
		}
	},
	showUpdater:function(node,val) {	//更新h-show指令所在节点的显隐情况（视图层）
		if(val){
			node.style.display = ""	//这里设置display值为空原因是如果设置为其他如‘block’会将元素改变为块级元素
		}else{
			node.style.display = "none"
		}
	},
	forUpdater:function(node,exp,val,itemKey,indexKey) { //更新h-for指令所在节点的渲染（视图层）
		if(!val || typeof val != 'object'){
			 console.error(exp+'找不到或格式不对!')
			 return
		}
		console.log(itemKey,indexKey,val[1])
// 		val.forEach(function(item) {
// 			var newNode = node.cloneNode(true) 
// 		})
		
	}
}